<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage core
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

/**
 * The mgDictionary is a dictionary class which can convert a symbol into a string, given a certain {@link mgLocale}.
 * When no {@link mgLocale} is specified, the runtime {@link mgLocale} is used. By implementing the {@link mgL10N}
 * interface, the mgDictionary class is localizable. Any changes in the runtime locale setting will directly
 * result into a notification of the mgDictionary class.
 *
 * For retrieving its symbols, a mgDictionary needs to have a {@link mgDictionarySource}. The symbols in this source
 * will be cached by the mgDictionary.
 *
 * @author Gido Hakvoort
 * @version 1.0
 * @see mgLocale
 * @see mgG11N
 * @see mgL10N
 */

namespace mg;

interface Dictionary extends L10N {

    public function translate($symbol, array $parameters = array(), Locale $locale = null);

}

class DefaultDictionary implements Dictionary {

    /**
     * @var Storage
     */
    private $symbols = null;

    /**
     * @var Storage
     */
    private $properties = null;

    /**
     * The current Locale
     *
     * @var Locale
     */
    private $locale = null;

    private $dictionarySources = array();

    /**
     * Create the mgDictionary instance
     *
     * @access public
     */
    public function __construct(array $dictionarySources) {
        $storageManager = in(function(StorageManager $storageManager) { return $storageManager; } );
        $g11n = in(function(G11N $g11n) { return $g11n; } );
        $runtime = in(function(Runtime $runtime) { return $runtime; });

        $symbols = $storageManager->getStorage('defaultdictionary.symbols', true);
        $this->symbols = $storageManager->cache($symbols);

        $properties = $storageManager->getStorage('defaultdictionary.properties', true);
        $this->properties = $storageManager->cache($properties);

        $g11n->addL10NListener($this);
        $this->locale = $g11n->getLocale();

        if($runtime->isTransactional()) {
            $this->rebuild($dictionarySources);
        }

    }

    protected function rebuild(array $dictionarySources) {
        $symbolSet = isset($this->properties['symbollist']) ? unserialize($this->properties['symbollist']) : array();

        $newSymbolSet = array();

        /* @var $dictionarySource DictionarySource */
        foreach($dictionarySources as $dictionarySource) {
            $symbols = $dictionarySource->getSymbols();

            foreach($symbols as $symbol => $value) {
                $symbol = mb_strtolower($symbol);

                unset($symbolSet[$symbol]);

                // agressive write (optimizable)
                $this->symbols[$symbol] = $value;
                $newSymbolSet[$symbol] = true;
            }

        }

        foreach($symbolSet as $symbol => $_) {
            unset($this->symbols[$symbol]);
        }

        $this->properties['symbollist'] = serialize($newSymbolSet);

    }

    /**
     * Set the {@link mgLocale} used by the mgDictionary. This method will
     * be called when the applicate wide locale will be modified.
     *
     * @access public
     * @see L10N
     * @see G11N
     * @param \mg\Locale $locale
     */
    public function localeChanged(Locale $locale) {
        $this->locale = $locale;
    }

    /**
     * Translate a symbol into a localized string. Optional parameters can be given to be evaluated in this string and a {@link Locale}
     * can also be passed, to specify a fixed {@link Locale} for a string. When no {@link Locale} is passed, the {@link Locale}
     * set by {@link G11N} is used.
     *
     * To insert parameters into the translation of the symbol, the translation of the symbol should adhere to the printf string formatting format.
     *
     * @param string $symbol The symbol to translate
     * @param array $parameters The parameters to insert into the translated symbol
     * @param Locale $locale The {@link Locale} to use, when not specified use the {@link Locale} set through {@link Dictionary :: setLocale()}.
     * @return string The translation of the symbol
     */
    public function translate($symbol, array $parameters = array(), Locale $locale = null) {
        if($parameters !== null && !(is_array($parameters) || ($parameters instanceof \ArrayIterator))) {
            throw new Exception("\$parameters must be an array or an instance of ArrayIterator");
        }

        // TODO : think about this
        if($parameters instanceof \ArrayIterator) {
            $parameters = $parameters->getArrayCopy();
        }
        /* @var $locale Locale */
        $locale = ($locale !== null ? $locale : $this->locale);

        if($locale === null) {
            return null;
        }

        $symbol = mb_strtolower(trim($symbol));

        $language = $locale->getLanguage();

        if(!isset($this->symbols[$language.'.'.$symbol])) {
            return null;
        }

        $message = $this->symbols[$language.'.'.$symbol];

        return \MessageFormatter :: formatMessage((string) $locale, $message, $parameters);
    }

}

/**
 * The DictionarySource is a source of symbols for the {@link DefaultDictionary}. Upon deployment the
 * DefaultDictionarySource will be queried for updates. If updates are available, the symbols inside
 * the DictionarySource will be read and cached by the {@link DefaultDictionary}.
 *
 * @author Michiel Hakvoort
 * @version 1.0
 * @see Dictionary
 */
interface DictionarySource {

    /**
     * Get the symbols in this DictionarySource. The symbols array is of the type "string -> string".
     * The key should be of the format [language].[symbol], the value can be any string but also allows
     * for string formatting.
     *
     * @access public
     * @return array The symbols defined by the DictionarySource
     */
    public function getSymbols();

}

/**
 * The mgFileSystemDictionarySource is a {@link mgDictionarySource} which reads symbols from all language files
 * within a folder. The format of the language file is that of a normal ini file. For example
 *
 * [nl]
 * WELCOME = "welkom %s"
 *
 * [en]
 * WELCOME = "welcome %s"
 *
 * Defines a welcome message in both english and dutch.
 *
 * When scanning the folder for language files, all files with the .lang extension will be parsed.
 *
 * @author Michiel Hakvoort
 * @author Gido Hakvoort
 * @version 1.0
 * @see DictionarySource
 * @see Dictionary
 */
class FileSystemDictionarySource implements DictionarySource {

    private $path = null;
    private $symbols = array();

    /**
     * Create a new mgFileSystemDictionarySource which extracts the symbols from language files
     * found in the given dictionary directory.
     *
     * @access public
     * @param string $dictionaryDir The directory to recursively search for language files
     */
    public function __construct($path) {
        $canonicalPath = realpath($path);

        if($canonicalPath === false) {
            throw new Exception("Path \"{$path}\" does not exist");
        }

        $this->path = $canonicalPath;
    }

    /**
     * {@inheritDoc}
     */
    public function getSymbols() {
        $path = $this->path;

        return in(function(StorageManager $storageManager, FileSystem $fileSystem) use($path) {

            $parsedFiles = $storageManager->getStorage('filesystemdictionarysource.parsedfiles', false);
            $cache = $storageManager->getStorage('filesystemdictionarysource.cache', false);

            $iterator = new \RecursiveIteratorIterator(
            new RegexRecursiveDirectoryFilterIterator(
            new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator :: CURRENT_AS_FILEINFO)
            ,'#^[^.].*$#i'
            ,'#^[^.].*\.lang$#i'));

            $result = array();

            $base64Path = base64_encode($path);

            $knownFiles = isset($parsedFiles[$base64Path]) ? unserialize($parsedFiles[$base64Path]) : array();

            $newParsedFiles = array();

            foreach($iterator as $filename => $file) {
                if(!$file->isReadable()) {
                    continue;
                }

                $encodedFilename = base64_encode($filename);

                unset($knownFiles[$encodedFilename]);

                $fileStatus = $fileSystem->getFileStatus($filename);

                $symbols = null;

                if($fileStatus === FileSystem :: FILE_NOT_MODIFIED) {
                    if(isset($cache[$encodedFilename])) {
                        $symbols = unserialize($cache[$encodedFilename]);
                    }
                }

                if($symbols === null) {
                    $symbols = array();

                    $ini_data = parse_ini_file($file, true);

                    foreach($ini_data as $locale=>$data) {
                        if(is_array($data)) {
                            foreach($data as $symbol=>$value) {
                                $symbols[$locale.'.'.$symbol] = $value;
                            }
                        }
                    }
                }

                $cache[$encodedFilename] = serialize($symbols);
                $newParsedFiles[$filename] = true;
                $result = $symbols + $result;
            }

            foreach($knownFiles as $knownFile => $value) {
                unset($cache[$knownFile]);
            }

            $parsedFiles[$base64Path] = serialize($newParsedFiles);

            return $result;
        });
    }
}
